﻿#include "InfFile.h"

#include <QDebug>
#include <QMessageBox>
#include "Utils.h"
#include <cstdlib>


///////////////////////////////////////////////////////////////
static bool GetVersionInfo(const char *pInfPath, const char *pKey, char **ppValue, bool showErrors)
{
    if (!pInfPath)
        return false;

    if (*ppValue)
        return true;

    DWORD size;
    PSP_INF_INFORMATION pInformation;

    if (!SetupGetInfInformation(pInfPath, INFINFO_INF_NAME_IS_ABSOLUTE, nullptr, 0, &size)) {
        if (showErrors)
            qDebug("SetupGetInfInformation() for %s", pInfPath);
        return false;
    }

    pInformation = (PSP_INF_INFORMATION)LocalAlloc(LPTR, size);

    if (pInformation) {
        if (!SetupGetInfInformation(pInfPath, INFINFO_INF_NAME_IS_ABSOLUTE, pInformation, size, nullptr)) {
            if (showErrors)
                qDebug("SetupGetInfInformation() for %s", pInfPath);
            LocalFree(pInformation);
            return false;
        }
    } else {
        SetLastError(ERROR_NOT_ENOUGH_MEMORY);
        qDebug("LocalAlloc(%lu)", (unsigned long)size);
        return false;
    }

    if (!SetupQueryInfVersionInformation(pInformation, 0, pKey, nullptr, 0, &size)) {
        if (showErrors)
            qDebug("SetupQueryInfVersionInformation(%s) for %s", pKey, pInfPath);
        LocalFree(pInformation);
        return false;
    }

    *ppValue = (char *)LocalAlloc(LPTR, size*sizeof(*ppValue[0]));

    if (*ppValue) {
        if (!SetupQueryInfVersionInformation(pInformation, 0, pKey, *ppValue, size, nullptr)) {
            if (showErrors)
                qDebug("SetupQueryInfVersionInformation(%s) for %s", pKey, pInfPath);
            LocalFree(*ppValue);
            *ppValue = nullptr;
            LocalFree(pInformation);
            return false;
        }
    } else {
        SetLastError(ERROR_NOT_ENOUGH_MEMORY);
        qDebug("LocalAlloc(%lu)", (unsigned long)(size*sizeof(*ppValue[0])));
        LocalFree(pInformation);
        return false;
    }

    LocalFree(pInformation);
    return true;
}
///////////////////////////////////////////////////////////////
static bool Open(const char *pInfPath, HINF *phInf, bool showErrors)
{
    if (*phInf != INVALID_HANDLE_VALUE)
        return true;

    UINT errLine;
    *phInf = SetupOpenInfFile(pInfPath, nullptr, INF_STYLE_WIN4, &errLine);

    if (*phInf == INVALID_HANDLE_VALUE) {
        qDebug("SetupOpenInfFile(%s) on line %u", pInfPath, errLine);
    }

    return *phInf != INVALID_HANDLE_VALUE;
}
///////////////////////////////////////////////////////////////
static bool IsPathInList(
    const char *pPath,
    const char *const *ppList)
{
    if (pPath == nullptr || ppList == nullptr)
        return false;

    while (*ppList) {
        if (lstrcmpi(*ppList++, pPath) == 0)
            return true;
    }

    return false;
}
///////////////////////////////////////////////////////////////
static bool GetFilePath(
    const char *pFileName,
    const char *pNearPath,
    char *pFilePath,
    DWORD lenFilePath)
{
    char *pBuf;
    DWORD res;

    // 若不指定文件所在路径
    if (!pNearPath) {
        if (DWORD(lstrlen(pFileName)) >= lenFilePath) {
            qDebug("lstrlen(%s) >= %lu", pFileName, (long)lenFilePath);
            return false;
        }
        // 默认在应用程序目录下
        lstrcpy(pFilePath, pFileName);
        return true;
    }

    // 获取文件名/目录名pNearPath的全路径到pFilePath，其中pBuf指向文件名起始位
    res = GetFullPathName(pNearPath, lenFilePath, pFilePath, &pBuf);

    if (!res) {
        qDebug("GetFullPathName(%s) failed, lastError=%lu", pNearPath, GetLastError());
        return false;
    }
    if (res >= lenFilePath) {
        qDebug("GetFullPathName(%s) truncated (buffer too small)", pNearPath);
        return false;
    }

    // 如果 pBuf 非空，则 pNearPath 指向一个文件。使用 pBuf 之前的目录部分。
    // 如果 pBuf 为null，则 pNearPath 为目录路径；使用整个 pFilePath 作为基础。
    DWORD basePathLen = pBuf ? DWORD(pBuf - pFilePath) : DWORD(lstrlen(pFilePath));

    // 确保我们有空间用于可选的分隔符+文件名+终止符
    DWORD fileLen = DWORD(lstrlen(pFileName));
    bool needSep = (basePathLen > 0) && (pFilePath[basePathLen - 1] != '\\') && (pFilePath[basePathLen - 1] != '/');
    // 空终止符 +1
    DWORD needed = basePathLen + (needSep ? 1 : 0) + fileLen + 1;
    if (needed > lenFilePath) {
        qDebug("Path buffer too small for %s + %s", pNearPath, pFileName);
        return false;
    }

    // 在basePathLen处终止（如果存在则切断文件部分）
    pFilePath[basePathLen] = '\0';

    // 添加分隔符
    if (needSep) {
        pFilePath[basePathLen++] = '\\';
        pFilePath[basePathLen] = '\0';
    }

    // 添加文件名
    lstrcpy(pFilePath + basePathLen, pFileName);

    return true;
}

InfFile::InfFile(const char *pInfName, const char *pNearPath)
    : pPath(nullptr)
    , pOemPath(nullptr)
    , pClassGUID(nullptr)
    , pClass(nullptr)
    , pProvider(nullptr)
    , pDriverVer(nullptr)
    , pUninstallInfTag(nullptr)
    , hInf(INVALID_HANDLE_VALUE)
{
    char path[MAX_PATH + 1];

    if (GetFilePath(pInfName, pNearPath, path, sizeof(path)/sizeof(path[0])))
        pPath = _strdup(path);
}
///////////////////////////////////////////////////////////////
InfFile::~InfFile()
{
    if (pPath)
        free(pPath);
    if (pOemPath)
        // _strdup
        free(pOemPath);
    if (pClassGUID)
        LocalFree((HLOCAL)pClassGUID);
    if (pClass)
        LocalFree((HLOCAL)pClass);
    if (pProvider)
        LocalFree((HLOCAL)pProvider);
    if (pUninstallInfTag)
        LocalFree((HLOCAL)pUninstallInfTag);
    if (pDriverVer)
        LocalFree((HLOCAL)pDriverVer);
    if (hInf != INVALID_HANDLE_VALUE)
        SetupCloseInfFile(hInf);
}
///////////////////////////////////////////////////////////////
bool InfFile::Test(const InfFileField *pFields, bool showErrors) const
{
    if (!Open(pPath, &hInf, showErrors)) {
        qDebug() << "Warning: Open Inf File Failed, because" << GetLastError();
        return false;
    }

    for (const InfFileField *pField = pFields ; pField->pSection != nullptr ; pField++) {
        INFCONTEXT context;

        // 检查指定Section的首行中Key键名是否存在
        if (!SetupFindFirstLine(hInf, pField->pSection, pField->pKey, &context))
            return false;

        for (;;) {
            DWORD size;

            // 获取指定字段所需缓冲区大小
            if (!SetupGetStringField(&context, pField->nField, nullptr, 0, &size))
                return false;

            char *pValue = (char *)LocalAlloc(LPTR, size*sizeof(pValue[0]));

            if (pValue == nullptr) {
                SetLastError(ERROR_NOT_ENOUGH_MEMORY);
                qDebug("LocalAlloc(%lu)", (unsigned long)(size*sizeof(pValue[0])));
                return false;
            }

            // 获取指定字段
            if (!SetupGetStringField(&context, pField->nField, pValue, size, nullptr)) {
                LocalFree(pValue);
                return false;
            }

            // 验证比较
            if (lstrcmpi(pValue, pField->pFieldValue) == 0) {
                LocalFree(pValue);
                break;
            }

            LocalFree(pValue);

            // 继续查找下一个key相同的行
            if (!SetupFindNextMatchLine(&context, pField->pKey, &context))
                return false;
        }
    }

    return true;
}
///////////////////////////////////////////////////////////////
const char *InfFile::OemPath(bool showErrors) const
{
    if (!pOemPath) {
        char path[MAX_PATH + 1];

        if (SetupCopyOEMInf(pPath, nullptr, SPOST_NONE, SP_COPY_REPLACEONLY|SP_COPY_NOOVERWRITE,
                            path, sizeof(path)/sizeof(path[0]), nullptr, nullptr) ||
            GetLastError() == ERROR_FILE_EXISTS)
        {
            pOemPath = _strdup(path);
        }
    }

    return pOemPath;
}
///////////////////////////////////////////////////////////////
const char *InfFile::ClassGUID(bool showErrors) const
{
    GetVersionInfo(pPath, "ClassGUID", &pClassGUID, showErrors);

    return pClassGUID;
}
///////////////////////////////////////////////////////////////
const char *InfFile::Class(bool showErrors) const
{
    GetVersionInfo(pPath, "Class", &pClass, showErrors);

    return pClass;
}
///////////////////////////////////////////////////////////////
const char *InfFile::Provider(bool showErrors) const
{
    GetVersionInfo(pPath, "Provider", &pProvider, showErrors);

    return pProvider;
}
///////////////////////////////////////////////////////////////
const char *InfFile::DriverVer(bool showErrors) const
{
    GetVersionInfo(pPath, "DriverVer", &pDriverVer, showErrors);

    return pDriverVer;
}
///////////////////////////////////////////////////////////////
const char *InfFile::UninstallInfTag(bool showErrors) const
{
    GetVersionInfo(pPath, "UninstallInfTag", &pUninstallInfTag, showErrors);

    return pUninstallInfTag;
}
///////////////////////////////////////////////////////////////
static UINT FileCallback(
    PVOID Context,
    UINT Notification,
    UINT_PTR Param1,
    UINT_PTR Param2)
{
    if (Notification == SPFILENOTIFY_ENDDELETE) {
        PFILEPATHS pFilePaths = (PFILEPATHS)Param1;

        if (pFilePaths->Win32Error == ERROR_SUCCESS) {
            qDebug("Deleted File %s\n", pFilePaths->Target);
        }
        else
            if (pFilePaths->Win32Error == ERROR_FILE_NOT_FOUND) {
                qDebug("File %s not installed\n", pFilePaths->Target);
            }
            else {
                qDebug("Delete(%s) winErrCode %d", pFilePaths->Target, pFilePaths->Win32Error);
            }
    }

    return SetupDefaultQueueCallback(Context, Notification, Param1, Param2);
}

bool InfFile::UninstallFiles(const char *pFilesSection) const
{
    if (!pPath)
        return false;

    if (!Open(pPath, &hInf, true))
        return false;

    int res;

    do {
        res = IDCONTINUE;

        HSPFILEQ hFileQueue = SetupOpenFileQueue();

        if (hFileQueue != INVALID_HANDLE_VALUE) {
            if (SetupQueueDeleteSection(hFileQueue, hInf, nullptr, pFilesSection)) {
                PVOID pContext = SetupInitDefaultQueueCallback(nullptr);

                if (pContext) {
                    if(!SetupCommitFileQueue(nullptr, hFileQueue, FileCallback, pContext))
                        qDebug("SetupCommitFileQueue()");

                    SetupTermDefaultQueueCallback(pContext);
                } else {
                    qDebug("SetupInitDefaultQueueCallback()");
                }
            } else {
                qDebug("SetupQueueDeleteSection(%s)", pFilesSection);
            }

            SetupCloseFileQueue(hFileQueue);
        } else {
            qDebug("SetupOpenFileQueue()");
        }
    } while (res == IDTRYAGAIN);

    if (res != IDCONTINUE)
        return false;

    return true;
}
///////////////////////////////////////////////////////////////
bool InfFile::InstallOEMInf() const
{
    if (!pPath)
        return false;

    bool wasInstalled = (OemPath() != nullptr);

    if (SetupCopyOEMInf(pPath, nullptr, SPOST_PATH, 0, nullptr, 0, nullptr, nullptr) != ERROR_SUCCESS) {
        qDebug() << QString("SetupCopyOEMInf(%1) Because").arg(pPath) << Utils::formatWinError(GetLastError());
        return false;
    }

    if (!wasInstalled)
        qDebug("Installed %s to %s\n", pPath, OemPath());
    else
        qDebug("Re-installed %s to %s\n", pPath, OemPath());

    return true;
}
///////////////////////////////////////////////////////////////
static bool UninstallFile(const char *pPath)
{
    int res;

    do {
        res = IDCONTINUE;

        if (DeleteFile(pPath)) {
            qDebug("Deleted %s\n", pPath);
        }
        else
            if (GetLastError() == ERROR_FILE_NOT_FOUND) {
                qDebug("File %s not installed\n", pPath);
            }
            else {
                qDebug("DeleteFile(%s)", pPath);
            }
    } while (res == IDTRYAGAIN);

    if (res != IDCONTINUE)
        return false;

    return true;
}

static bool UninstallInf(const char *pPath)
{
    if (pPath == nullptr)
        return false;

    int res;

    do {
        res = IDCONTINUE;

        char infPathDest[MAX_PATH + 1];

        if (snprintf(infPathDest, sizeof(infPathDest)/sizeof(infPathDest[0]), "%s", pPath) > 0) {
            typedef BOOL (WINAPI *PSETUPUNINSTALLOEMINFA)(IN PCSTR, IN DWORD, IN PVOID);
            static PSETUPUNINSTALLOEMINFA pSetupUninstallOEMInf = nullptr;

            if(!pSetupUninstallOEMInf) {
                HMODULE hModule = GetModuleHandle("setupapi.dll");

                if (hModule) {
                    pSetupUninstallOEMInf =
                        (PSETUPUNINSTALLOEMINFA)GetProcAddress(hModule, "SetupUninstallOEMInfA");
                }
            }

            if (pSetupUninstallOEMInf) {
                char *pInfNameDest, *p;

                for (pInfNameDest = p = infPathDest ; *p ; p++)
                    if (*p == '\\')
                        pInfNameDest = p + 1;

                if (pSetupUninstallOEMInf(pInfNameDest, 0, nullptr)) {
                    qDebug("Uninstalled %s\n", pInfNameDest);
                } else {
                    qDebug("SetupUninstallOEMInf(%s)", pInfNameDest);
                }
            }
            else
                if (UninstallFile(infPathDest)) {
                    int infPathDestLen = lstrlen(infPathDest);

                    if (infPathDestLen > 4) {
                        char *pInfPathDestExt = infPathDest + infPathDestLen - 4;

                        if (!lstrcmpi(pInfPathDestExt, ".inf")) {
                            pInfPathDestExt[1] = 'p';     // pInfPathDestExt = ".pnf"

                            if (!UninstallFile(infPathDest))
                                res = IDCANCEL;
                        }
                    }
                } else {
                    res = IDCANCEL;
                }
        } else {
            qDebug("Can't uninstall %s\n", pPath);
            res = IDCANCEL;
        }
    } while (res == IDTRYAGAIN);

    if (res != IDCONTINUE)
        return false;

    return true;
}
///////////////////////////////////////////////////////////////
bool InfFile::UninstallOEMInf() const
{
    if (!pPath)
        return false;

    int res;

    do {
        res = IDCONTINUE;

        if (OemPath()) {
            UninstallInf(OemPath());
        } else {
            if (GetLastError() == ERROR_FILE_NOT_FOUND) {
                qDebug("File %s not installed\n", pPath);
            } else {
                qDebug("SetupCopyOEMInf(%s)", pPath);
            }
        }
    } while (res == IDTRYAGAIN);

    if (res != IDCONTINUE)
        return false;

    return true;
}
///////////////////////////////////////////////////////////////
static bool TestUninstall(
    const InfFile &infFile,
    const InfFile::InfFileUninstall *pInfFileUninstallList,
    bool queryConfirmation)
{
    for (
        const InfFile::InfFileUninstall *pInfFileUninstall = pInfFileUninstallList ;
        pInfFileUninstall->pRequiredFields != nullptr ;
        pInfFileUninstall++)
    {
        if (pInfFileUninstall->queryConfirmation != queryConfirmation)
            continue;

        if (infFile.Test(pInfFileUninstall->pRequiredFields, false))
            return true;
    }

    return false;
}

bool InfFile::UninstallAllInfFiles(
    const InfFileUninstall *pInfFileUninstallList,
    const char *const *ppOemPathExcludeList)
{
    qDebug("Scan INF files .");

    DWORD size;

    if (!SetupGetInfFileList(nullptr, INF_STYLE_WIN4, nullptr, 0, &size)) {
        DWORD err = GetLastError();

        qDebug(" FAIL\n");
        qDebug("SetupGetInfFileList() errCode %d", err);
        return false;
    }

    qDebug("...");

    size += 256; // possible new INF files were added since
    char *pList = (char *)LocalAlloc(LPTR, size*sizeof(pList[0]));

    if (pList) {
        if (!SetupGetInfFileList(nullptr, INF_STYLE_WIN4, pList, size, nullptr)) {
            DWORD err = GetLastError();

            qDebug(" FAIL\n");
            qDebug("SetupGetInfFileList(%lu) errCode %d", (unsigned long)size, err);
            LocalFree(pList);
            return false;
        }
    } else {
        qDebug(" FAIL\n");
        SetLastError(ERROR_NOT_ENOUGH_MEMORY);
        qDebug("LocalAlloc(%lu)", (unsigned long)size);
        return false;
    }

    qDebug(".");

    char windir[MAX_PATH + 1];

    size = GetEnvironmentVariable("windir", windir, sizeof(windir)/sizeof(windir[0]));

    if (!size || size >= sizeof(windir)/sizeof(windir[0])) {
        DWORD err = !size ? GetLastError() : ERROR_BUFFER_OVERFLOW;

        qDebug(" FAIL\n");
        qDebug("GetEnvironmentVariable(windir) errCode %d", err);
        LocalFree(pList);
        return false;
    }

    char *p = pList;
    int i;
    int m;

    p = pList;
    i = 0;

    do {
        i++;
        p += lstrlen(p) + 1;
    } while (*p);

    m = i/3;

    if (m == 0)
        m = 1;

    p = pList;
    i = 0;

    do {
        if (++i%m == 0)
            qDebug(".");

        char infPath[MAX_PATH + 1];

        if ((p[0] == 'O' || p[0] == 'o') &&
            (p[1] == 'E' || p[1] == 'e') &&
            (p[2] == 'M' || p[2] == 'm') &&
            snprintf(infPath, sizeof(infPath)/sizeof(infPath[0]), "%s\\inf\\%s", windir, p) > 0)
        {
            InfFile infFile(infPath, nullptr);
            bool doUninstall;

            if (IsPathInList(infFile.Path(), ppOemPathExcludeList)) {
                //qDebug("\nSkipped %s\n", infFile.Path());
                doUninstall = false;
            }
            else
                if (TestUninstall(infFile, pInfFileUninstallList, false)) {
                    doUninstall = true;
                }
                else
                    if (TestUninstall(infFile, pInfFileUninstallList, true)) {
                        // 使用 Qt 的 QMessageBox 替换原始 ShowMsg 提示
                        const QString text = QString::asprintf(
                            "The file %1 possible should be deleted too.\n\n"
                            "%2:\n"
                            "  ClassGUID = %3\n"
                            "  Class = %4\n"
                            "  Provider = %5\n"
                            "  DriverVer = %6\n"
                            "  UninstallInfTag = %7\n\n"
                            "Would you like to delete it?\n"
                            , infFile.Path()
                            , infFile.Path()
                            , infFile.ClassGUID(false)
                            , infFile.Class(false)
                            , infFile.Provider(false)
                            , infFile.DriverVer(false)
                            , infFile.UninstallInfTag(false));

                        doUninstall = Utils::askYesNoDialog(
                            QStringLiteral("Delete INF File"),
                            text,
                            QMessageBox::No);
                    } else {
                        doUninstall = false;
                    }

            if (doUninstall) {
                qDebug("\n");

                if (infFile.hInf != INVALID_HANDLE_VALUE) {
                    SetupCloseInfFile(infFile.hInf);
                    infFile.hInf = INVALID_HANDLE_VALUE;
                }

                UninstallInf(infFile.Path());
            }
        }

        p += lstrlen(p) + 1;
    } while (*p);

    qDebug(" done.\n");

    LocalFree(pList);

    return true;
}
///////////////////////////////////////////////////////////////
